package com.ruoyi.framework.security.controller;

import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.framework.config.OAuth2CacheManager;
import com.ruoyi.framework.config.OAuth2SecurityConfig;
import com.ruoyi.framework.config.properties.KeycloakConfig;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.TokenService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.Map;

@RestController
@RequestMapping("/sso/oauth2")
@Slf4j
public class OAuth2Controller {

    @Resource
    private ClientRegistrationRepository clientRegistrationRepository;

    @Resource
    private KeycloakConfig keycloakConfig;

    @Resource
    private OAuth2CacheManager oauth2CacheManager;

    @Resource
    private TokenService tokenService;

    @Resource
    private SessionRegistry sessionRegistry;

    private static final long ALLOWED_CLOCK_SKEW_SECONDS = 60;

    /**
     * 没有具体业务，只是为了触发 spring security 的登录流程
     * 前端通过 window.location.href 的方式调用此接口来触发登录流程，避免重定向跨域问题
     * 正常情况下未登录调用此接口会被security拦截，然后重定向到身份认证的登录页面；如果进入了该接口里面了则说明security身份认证是通过的
     * 为防止系统原来的token失效但security oauth2的access token未失效导致不同步问题（会在这个接口和前端的 / 路由之间反复重定向）
     * 这里判断如果系统原本的token失效，则清除security的context，让其重新登录
     */
    @GetMapping("/loginEnter")
    public String ssoLogin(@RequestParam("origin") String origin, @RequestParam("token") String token,
                           HttpServletRequest request, HttpServletResponse response) throws IOException {
        if(StringUtils.isBlank(token) || token.equals("null") || token.equals("undefined")){
            SecurityContextHolder.getContext().getAuthentication().setAuthenticated(false);
            request.getSession().invalidate();
        }
        response.sendRedirect(origin + "/");
        return "oauth2 login";
    }

    @PostMapping(value = "/backChannelLogout")
    public ResponseEntity<Object> handleBackChannelLogout(HttpServletRequest request) {
        try {
            String logoutToken = request.getParameter("logout_token");
            if (logoutToken == null || logoutToken.trim().isEmpty()) {
                return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
            }

            JWTClaimsSet jwtClaimsSet = validateLogoutToken(logoutToken);
            String sid = jwtClaimsSet.getStringClaim("sid");
            if (StringUtils.isNotBlank(sid)) {
                try{
                    oauth2CacheManager.clearCache(request, sid);
                }catch (Exception e){
                    log.error(e.getMessage(), e);
                }

                // 让 oauth2 登录的会话失效
                String sessionId = OAuth2SecurityConfig.sessionMap.get(sid);
                if (StringUtils.isNotBlank(sessionId)) {
                    SessionInformation session = sessionRegistry.getSessionInformation(sessionId);
                    if (session != null) {
                        OAuth2SecurityConfig.sessionMap.remove(sid);
                        session.expireNow();
                    }
                }

                // 让系统原本的登录 token 失效
                String token = OAuth2SecurityConfig.tokenMap.get(sid);
                if (StringUtils.isNotBlank(token)) {
                    String userName = tokenService.getUsernameFromToken(token);
                    tokenService.delLoginUser(token);
                    AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
                    OAuth2SecurityConfig.tokenMap.remove(sid);
                }

                return new ResponseEntity<>(HttpStatus.OK);
            }

            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        } catch (Exception e) {
            log.error("Error validating logout token", e);
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    // 根据OIDC标准验证logoutToken的有效性，参考： https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest
    private JWTClaimsSet validateLogoutToken(String logoutToken) throws Exception {
        ConfigurableJWTProcessor<com.nimbusds.jose.proc.SecurityContext> jwtProcessor = getSecurityContextConfigurableJWTProcessor();
        JWTClaimsSet jwtClaimsSet = jwtProcessor.process(logoutToken, null);

        ClientRegistration registration = this.clientRegistrationRepository.findByRegistrationId(keycloakConfig.getProvider());
        if(registration != null){
            if (!jwtClaimsSet.getIssuer().equals(keycloakConfig.getIssuerUri())) {
                throw new JwtException("Invalid issuer in logout token");
            }
            if (!jwtClaimsSet.getAudience().contains(registration.getClientId())) {
                throw new JwtException("Invalid audience in logout token");
            }

            Date issueTime = jwtClaimsSet.getIssueTime();
            Date expirationTime = jwtClaimsSet.getExpirationTime();
            if (issueTime == null || expirationTime == null) {
                throw new JwtException("Missing required time claims in logout token");
            }
            Instant issuedAt = issueTime.toInstant();
            Instant expiration = expirationTime.toInstant();
            Instant now = Instant.now();
            Instant nowWithSkewMin = now.minusSeconds(ALLOWED_CLOCK_SKEW_SECONDS);
            Instant nowWithSkewMax = now.plusSeconds(ALLOWED_CLOCK_SKEW_SECONDS);
            // 验证签发时间（允许一定误差）
            if (issuedAt.isAfter(nowWithSkewMax)) {
                long secondsOff = Duration.between(now, issuedAt).getSeconds();
                System.out.println("IssuedAt is after current time by " + secondsOff + " seconds (allowed skew: " + ALLOWED_CLOCK_SKEW_SECONDS + ")");
                throw new JwtException("Issued at time is too far in the future");
            }
            // 验证过期时间（允许一定误差）
            if (expiration.isBefore(nowWithSkewMin)) {
                long secondsOff = Duration.between(expiration, now).getSeconds();
                System.out.println("Expiration is before current time by " + secondsOff + " seconds (allowed skew: " + ALLOWED_CLOCK_SKEW_SECONDS + ")");
                throw new JwtException("Token has expired");
            }

            try {
                Map<String, Object> events = jwtClaimsSet.getJSONObjectClaim("events");
                if (!events.containsKey("http://schemas.openid.net/event/backchannel-logout")) {
                    throw new JwtException("Invalid events claim in logout token");
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            if (jwtClaimsSet.getClaims().containsKey("nonce")) {
                throw new JwtException("Nonce claim should not be present in logout token");
            }
        }

        return jwtClaimsSet;
    }

    // 自定义的JWT处理器，让其支持OIDC的logout+jwt格式
    private ConfigurableJWTProcessor<com.nimbusds.jose.proc.SecurityContext> getSecurityContextConfigurableJWTProcessor() throws MalformedURLException {
        ConfigurableJWTProcessor<com.nimbusds.jose.proc.SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        JOSEObjectTypeVerifier<com.nimbusds.jose.proc.SecurityContext> typeVerifier =
                new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("logout+jwt"), null);
        jwtProcessor.setJWSTypeVerifier(typeVerifier);
        jwtProcessor.setJWETypeVerifier(typeVerifier);
        jwtProcessor.setJWSKeySelector(
                new JWSVerificationKeySelector<>(
                        JWSAlgorithm.RS256,
                        new RemoteJWKSet<>(new URL(keycloakConfig.getJwkSetUri()))
                )
        );
        return jwtProcessor;
    }

}

